<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Symbol</title>
</head>
<body>
<script type="text/javascript">
    {
        /**
         * Symbol:新的原始数据类型，表示独一无二的值，是JavaScript语言的第七种数据类型。
         * 数据类型：Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)
         * Symbol值通过Symbol函数生成，对象的属性名现在可以有两种类型，一种是原来就有的字符串，另一种就是新增的Symbol类型。凡是属性名属于Symbol类型，就都是独一无二的，可以保证不会与其他属性名产生冲突。
         */
        let s = Symbol();
        console.log(typeof s); // symbol

        /*Symbol函数前不能使用new命令，否则会报错。因为生成的Symbol是一个原始类型的值，不是对象。也就是说，由于Symbol值不是对象，所以不能添加属性。基本上，它是一种类似于字符串的数据类型。*/
        const s1 = Symbol();
        const s2 = Symbol();
        console.log(s1 === s2); // false

        /*Symbol函数可以接受一个字符串作为参数，表示对Symbol实例的描述，主要是为了在控制台显示，或者转为字符串时，比较容易区分*/
        const s3 = Symbol('foo');
        const s4 = Symbol('bar');
        console.log(s3, s4, s3.toString(), s4.toString()); // Symbol(foo) Symbol(bar) "Symbol(foo)" "Symbol(bar)"

        /*Symbol值不能与其他类型的值进行运算，会报错。但是，Symbol值可以显式转为字符串，另外，Symbol值也可以转为布尔值，但是不能转为数值。*/
        console.log(String(s3), Boolean(s1), !s2); // Symbol(foo) true false
    }
    {
        /**
         * 作为属性名的Symbol
         * 由于每一个Symbol值都是不相等的，这意味着Symbol值可以作为标识符，用于对象的属性名，就能保证不会出现同名的属性。
         * 这对于一个对象由多个模块构成的情况非常有用，能防止某一个键被不小心改写或覆盖。
         */
        let symbol = Symbol();
        /*第一种写法*/
        let a = {};
        a[symbol] = 'hello';
        /*第二种写法*/
        let b = {[symbol]: 'hello'};
        /*第三种写法*/
        let c = {};
        Object.defineProperty(c, symbol, {value: 'hello'})
        console.log(a[symbol], b[symbol], c[symbol]); // hello hello hello

        /*Symbol类型还可以用于定义一组常量，保证这组常量的值都是不相等的*/
        const levels = {
            DEBUG: Symbol('debug'),
            INFO: Symbol('info'),
            WARN: Symbol('warn')
        };
        console.log(levels.DEBUG, 'debug message'); // Symbol(debug) "debug message"
        console.log(levels.INFO, 'info message'); // Symbol(info) "info message"

        const COLOR_RED = Symbol();
        const COLOR_GREEN = Symbol();

        function getComplement(color) {
            switch (color) {
                case COLOR_RED:
                    return COLOR_GREEN;
                case COLOR_GREEN:
                    return COLOR_RED;
                default:
                    throw new Error('Undefined color');
            }
        }

        console.log(getComplement(COLOR_RED)); // Symbol()
        // console.log(getComplement('')); // Undefined color
    }
    {
        /**
         * 实例：消除魔术字符串
         * 魔术字符串：在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码，应该尽量消除魔术字符串，该由含义清晰的变量代替。
         */
        function getArea(shape, options) {
            let area = 0;
            if (shape === 'Triangle') {
                area = .5 * options.width * options.height;
            }
            return area;
        }

        console.log(getArea('Triangle', {width: 100, height: 100})); // 魔术字符串 5000

        /*常用的消除魔术字符串的方法，就是把它写成一个变量*/
        const shapeType = {
            triangle: 'Triangle'
        };

        function getArea2(shape, options) {
            let area = 0;
            if (shape === shapeType.triangle) {
                area = .5 * options.width * options.height;
            }
            return area;
        }

        console.log(getArea2(shapeType.triangle, {width: 100, height: 100})); // 5000
    }
    {
        /**
         * 属性名的遍历
         * Object.getOwnPropertySymbols方法，可以获取指定对象的所有 Symbol 属性名
         */
        const obj = {};
        const a = Symbol('a');
        const b = Symbol('b');

        obj[a] = 'Hello';
        obj[b] = 'World';

        const objectSymbols = Object.getOwnPropertySymbols(obj);
        console.log(objectSymbols); // [Symbol(a), Symbol(b)]

        /*另一个新的API，Reflect.ownKeys方法可以返回所有类型的键名，包括常规键名和 Symbol 键名*/
        let o = {
            [Symbol('my_key')]: 1,
            enum: 2,
            nonEnum: 3
        };
        console.log(Reflect.ownKeys(o)); // ["enum", "nonEnum", Symbol(my_key)]

        /*由于以 Symbol 值作为名称的属性，不会被常规方法遍历得到。我们可以利用这个特性，为对象定义一些非私有的、但又希望只用于内部的方法*/
        let size = Symbol('size');

        class Collection {
            constructor() {
                this[size] = 0;
            }

            add(item) {
                this[this[size]] = item;
                this[size]++;
            }

            static sizeOf(instance) {
                return instance[size];
            }
        }

        const x = new Collection();
        console.log(Collection.sizeOf(x)); // 0
        x.add('foo');
        console.log(Collection.sizeOf(x)); // 1
        console.log(Object.keys(x)); // ["0"]
        console.log(Object.getOwnPropertyNames(x)); // ["0"]
        console.log(Object.getOwnPropertySymbols(x)); // [Symbol(size)]
    }
    {
        /**
         * Symbol.for()：接受一个字符串作为参数，然后搜索有没有以该参数作为名称的Symbol值。如果有，就返回这个Symbol值，否则就新建并返回一个以该字符串为名称的Symbol值。
         * Symbol.keyFor()：返回一个已登记的 Symbol 类型值的key
         */
        const s1 = Symbol.for('foo');
        const s2 = Symbol.for('foo');
        console.log(s1 === s2); // true
        const s3 = Symbol('foo');
        console.log(Symbol.keyFor(s1), Symbol.keyFor(s3)); // foo undefined
    }
    {
        /**
         * 实例：模块的 Singleton 模式
         * Singleton模式指的是调用一个类，任何时候返回的都是同一个实例。
         */
    }
    {
        /** 内置的Symbol值 */

        /*1、Symbol.hasInstance：当其他对象使用instanceof运算符，判断是否为该对象的实例时，会调用这个方法*/
        class MyClass {
            [Symbol.hasInstance](foo) {
                return foo instanceof Array;
            }
        }

        console.log([1, 2, 3] instanceof new MyClass()); // true

        class Even {
            static [Symbol.hasInstance](obj) {
                return Number(obj) % 2 === 0;
            }
        }

        console.log(1 instanceof Even); // false
        console.log(2 instanceof Even); // true
        console.log(12345 instanceof Even); // false

        /*2、Symbol.isConcatSpreadable：表示该对象使用Array.prototype.concat()时，是否可以展开。*/
        let arr1 = ['c', 'd'];
        ['a', 'b'].concat(arr1, 'e');
        console.log(arr1[Symbol.isConcatSpreadable]); // undefined

        let arr2 = ['c', 'd'];
        arr2[Symbol.isConcatSpreadable] = false;
        ['a', 'b'].concat(arr2, 'e');
        console.log(['a', 'b'].concat(arr2, 'e')); // ["a", "b", ["c","d"], "e"]

        /*类似数组的对象也可以展开，但它的Symbol.isConcatSpreadable属性默认为false，必须手动打开。*/
        let obj = {length: 2, 0: 'c', 1: 'd'};
        console.log(['a', 'b'].concat(obj, 'e')); // ['a', 'b', obj, 'e']

        obj[Symbol.isConcatSpreadable] = true;
        console.log(['a', 'b'].concat(obj, 'e')); // ["a", "b", "c", "d", "e"]

        /*3、Symbol.species：该对象作为构造函数创造实例时，会调用这个方法。即如果this.constructor[Symbol.species]存在，就会使用这个属性作为构造函数，来创造新的实例对象*/
        /*Symbol.species属性默认的读取器如下*/
        /*static get [Symbol.species]()
        {
            return this;
        }*/

        /*4、Symbol.match：当执行str.match(myObject)时，如果该属性存在，会调用它，返回该方法的返回值*/
        class MyMatcher {
            [Symbol.match](string) {
                return 'hello world'.indexOf(string);
            }
        }

        console.log('e'.match(new MyMatcher())); // 1

        /*5、Symbol.replace：当该对象被String.prototype.replace方法调用时，会返回该方法的返回值*/
        const x = {};
        x[Symbol.replace] = (...s) => console.log(s);
        console.log('Hello'.replace(x, 'World')); // ["Hello", "World"]

        /*6、Symbol.search*/
        class MySearch {
            constructor(value) {
                this.value = value;
            }

            [Symbol.search](string) {
                return string.indexOf(this.value);
            }
        }

        console.log('foobar'.search(new MySearch('foo'))); // 0

        /*7、Symbol.split*/
        /*8、Symbol.iterator*/
        const myIterable = {};
        myIterable[Symbol.iterator] = function* () {
            yield 1;
            yield 2;
            yield 3;
        };
        console.log(...myIterable); // 1 2 3

        class Collection {
            * [Symbol.iterator]() {
                let i = 0;
                while (this[i] !== undefined) {
                    yield this[i];
                    ++i;
                }
            }
        }

        let myCollection = new Collection();
        myCollection[0] = 1;
        myCollection[1] = 2;
        for (let value of myCollection) {
            console.log(value); //1 2
        }

        /*9、Symbol.toPrimitive*/
        let primitive = {
            [Symbol.toPrimitive](hint) {
                switch (hint) {
                    case 'number':
                        return 123;
                    case 'string':
                        return 'str';
                    case 'default':
                        return 'default';
                    default:
                        throw new Error();
                }
            }
        };
        console.log(2 * primitive); // 246
        console.log(3 + primitive); // 3default
        console.log(primitive == 'default'); // true
        console.log(String(primitive)); // str

        /*10、Symbol.toStringTag*/
        console.log(({[Symbol.toStringTag]: 'Foo'}.toString())); // [object Foo]
        class Collection2 {
            get [Symbol.toStringTag]() {
                return 'xxx';
            }
        }

        const y = new Collection2();
        console.log(Object.prototype.toString.call(y)); // [object xxx]

        /*11、Symbol.unscopables*/
        console.log(Array.prototype[Symbol.unscopables]); // {copyWithin: true, entries: true, fill: true, find: true, findIndex: true, flat: true, flatMap: true, includes: true, keys: true, values: true}
        console.log(Object.keys(Array.prototype[Symbol.unscopables])); // ["copyWithin", "entries", "fill", "find", "findIndex", "flat", "flatMap", "includes", "keys", "values"]

    }
</script>
</body>
</html>